package bancosys.tec.security.impl.web.filter;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;

import javax.security.auth.Subject;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.utils.Tuple;

import org.apache.log4j.Logger;

import bancosys.tec.security.SecurityAPIMessages;
import bancosys.tec.security.SecurityException;
import bancosys.tec.security.SecurityService;
import bancosys.tec.security.SecurityURLMapping;
import bancosys.tec.security.authorization.ChineseWallPermission;
import bancosys.tec.security.authorization.UrlPermission;
import bancosys.tec.security.chinesewall.ChineseWallContext;
import bancosys.tec.security.chinesewall.ChineseWallContextHolder;
import bancosys.tec.security.impl.web.WebSecurityContext;
import bancosys.tec.security.impl.web.WebSecurityManager;
import bancosys.tec.security.web.ChineseWallSetupException;
import bancosys.tec.security.web.PageMapping;

/**
 * @created 26/03/2008
 * @author Gustavo Almeida
 */
public abstract class SecurityServletFilter implements Filter {
    private static Logger logger;

    public static final String LOGIN_URL = "loginUrl";

    public static final String PERMISSION_DENIED_URL = "permissionDeniedUrl";

    public static final String CHINESE_WALL_CONTEXT_SESSION_KEY =
            "bancosys.tec.security.web.SecurityServletFilter.CHINESE_WALL_CONTEXT_SESSION_KEY";

    private volatile String loginUrl;

    private volatile String permissionDeniedUrl;

    private String[] ignoredSuffixes = new String[]{ ".js", ".css", ".png", ".gif", ".jpg", "jpeg" };

    /**
     * Inicializa o filtro com os dados de configuração: <code>securityManager</code>, <code>loginUrl</code>,
     * <code>permissionDeniedUrl</code>.
     * 
     * @param filterConfig configuração do filtro.
     * @see Filter
     */
    public void init(FilterConfig filterConfig) {
        this.loginUrl = SecurityURLMapping.getInstance().getLoginURL();
        if (this.loginUrl == null) {
            this.loginUrl = this.getNotNull(filterConfig.getInitParameter(LOGIN_URL), "/login");
        }
        this.permissionDeniedUrl = SecurityURLMapping.getInstance().getUnauthorizedURL();
        if (this.permissionDeniedUrl == null) {
            this.permissionDeniedUrl = this.getNotNull(filterConfig.getInitParameter(PERMISSION_DENIED_URL), "/login");
        }
    }

    /**
     * @return the ignoredSuffixes
     */
    public String[] getIgnoredSuffixes() {
        return this.ignoredSuffixes;
    }

    /**
     * @param ignoredSuffixes the ignoredSuffixes to set
     */
    public void setIgnoredSuffixes(String[] ignoredSuffixes) {
        this.ignoredSuffixes = ignoredSuffixes;
    }

    /**
     * Devolve o security manager.
     * 
     * @return security manager.
     */
    protected abstract WebSecurityManager getSecurityManager();

    /**
     * Devolve uma collection de tuplas que, dado o nome da entidade cadastrado na credencial, contém os nomes dos filtros e os respectivos
     * parâmetros para os quais a propriedade contida na credencial se aplica.
     * 
     * @param entityName nome da entidade
     * @return collection de tuplas (nome do filtro, nome da propriedade do filtro). Null caso a propriedade não possa ser aplicada.
     */
    protected abstract Collection<Tuple<String, String>> getParameterMapping(String entityName);

    /**
     * Retorna o contexto de chinese wall.
     * 
     * @param subject subject.
     * @return contexto de chinese wall.
     * @throws ChineseWallSetupException quando o chinesewall nao pode ser inicializado.
     */
    protected ChineseWallContext getChineseWallContext(Subject subject) throws ChineseWallSetupException {
        ChineseWallContext chineseWallContext = new ChineseWallContext();
        Set<Object> permissions = subject.getPublicCredentials();
        for (Object permission : permissions) {
            if (permission instanceof ChineseWallPermission) {
                ChineseWallPermission chineseWallPermission = (ChineseWallPermission) permission;
                Collection<Tuple<String, String>> mappings = this.getParameterMapping(chineseWallPermission.getEntityName());
                if (mappings == null) {
                    throw new ChineseWallSetupException(SecurityAPIMessages.CHINESEWALL_PARAMETER_NULL.create(chineseWallPermission
                            .getEntityName()));
                }
                for (Tuple<String, String> mapping : mappings) {
                    if (chineseWallPermission.isFullAccess()) {
                        chineseWallContext.disableFilter(mapping.head());
                    } else {
                        chineseWallContext.addParameter(mapping.head(), mapping.tail(),
                                Collections.<Serializable> singleton(chineseWallPermission.getCredentialId()));
                    }
                }
            }
        }
        return chineseWallContext;
    }

    /**
     * @see Filter
     */
    public void destroy() {
        // vazio.

    }

    /**
     * Faz o forward ou redireciona
     * 
     * @param url String
     * @param request Request
     * @param response Response
     * @throws ServletException ServletException
     * @throws IOException IOException
     */
    protected void forwardOrRedirect(String url, HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {
        if (url.contains("://")) {
            response.sendRedirect(url);
        } else {
            RequestDispatcher dispatcher = request.getRequestDispatcher(this.loginUrl);
            dispatcher.forward(request, response);
        }
    }

    /**
     * Filtra o request, validando se o usuário é válido e tem acesso ao recurso requisitado.
     * 
     * @param request requisição.
     * @param response servlet response.
     * @param chain cadeia de filtros.
     * @throws IOException {@link Filter}
     * @throws ServletException {@link Filter}
     * @see Filter
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
        if (requestURI.endsWith(this.loginUrl) || this.isIgnored(requestURI)) {
            chain.doFilter(httpRequest, response);
            return;
        }
        WebSecurityContext securityContext = new WebSecurityContext(httpRequest, resp);
        Subject subject = this.loadSubject(securityContext);

        if (!this.validateSubject(subject)) {
            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            this.forwardOrRedirect(this.loginUrl, httpRequest, resp);
        } else if (!this.accessGranted(subject, httpRequest)) {
            if (response instanceof HttpServletResponse) {
                ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN);
            } else {
                this.forwardOrRedirect(this.permissionDeniedUrl, httpRequest, resp);
            }
        } else {
            try {
                this.getSecurityManager().getSecurityService().setCurrentThreadSubject(subject);
                this.setupChineseWall(subject, httpRequest);
                chain.doFilter(request, response);
            } catch (ChineseWallSetupException e) {
                this.forwardOrRedirect(this.permissionDeniedUrl, httpRequest, resp);
                this.getLogger().error("Erro inicializando chinese-wall.", e);
            } finally {
                this.clearChineseWall();
                this.getSecurityManager().getSecurityService().setCurrentThreadSubject(null);
            }
        }
    }

    /**
     * Carrega o subject utilizando o security manager
     * 
     * @param securityContext securityContext
     * @return Subject ou null
     * @throws ServletException caso ocorra uma exception dentro da chamada ao manager
     */
    private Subject loadSubject(WebSecurityContext securityContext) throws ServletException {
        try {
            return this.getSecurityManager().loadSubject(securityContext);
        } catch (SecurityException e) {
            throw new ServletException(e.getMessage(), e);
        }
    }

    /**
     * Verifica se a URL deve ser ignorada (por exemplo, javascript, etc). Por default, ignora css, javascript, gif, png e jpg
     * 
     * @param requestURI a url
     * @return boolean
     */
    private boolean isIgnored(String requestURI) {
        for (String suffix : this.ignoredSuffixes) {
            if (requestURI.endsWith(suffix)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Inicializa o contexto do chinese-wall e o associa a thread atual.
     * 
     * @param subject subject.
     * @param request request.
     * @throws ChineseWallSetupException quando o chinesewall nao pode ser inicializado.
     */
    protected void setupChineseWall(Subject subject, HttpServletRequest request) throws ChineseWallSetupException {
        ChineseWallContext chineseWallContext = (ChineseWallContext) request.getSession().getAttribute(CHINESE_WALL_CONTEXT_SESSION_KEY);
        if (chineseWallContext == null) {
            chineseWallContext = this.getChineseWallContext(subject);
            request.getSession().setAttribute(CHINESE_WALL_CONTEXT_SESSION_KEY, chineseWallContext);
        }
        ChineseWallContextHolder.set(chineseWallContext);
    }

    /**
     * Limpa o contexto do chinese-wall.
     */
    protected void clearChineseWall() {
        ChineseWallContextHolder.clear();
    }

    /**
     * Verifica se o subject é valido. Nessa implementação qualquer subject diferente de <code>null</code> é considerado válido.
     * 
     * @param subject subject.
     * @return <code>true</code> se o subject for válido, <code>false</code> caso contrário.
     */
    protected boolean validateSubject(Subject subject) {
        return subject != null;
    }

    /**
     * Verifica se o subject tem acesso ao recurso requisitado. Nessa implementação valida {@link UrlPermission}.
     * 
     * @param subject subject
     * @param request request
     * @return <code>true</code> se o subject tiver acesso ao recurso requisitado, <code>false</code> caso contrário.
     */
    protected boolean accessGranted(Subject subject, HttpServletRequest request) {
        if (subject == null) {
            return false;
        }
        final String path = request.getServletPath();
        if (!path.contains("/protected/")) {
            return true;
        }
        final PageMapping instance = PageMapping.getInstance();
        final SecurityService securityService = this.getSecurityManager().getSecurityService();
        return securityService.hasPermission(subject, UrlPermission.class, instance.getKeyForPageOrPage(path))
                || securityService.hasPermission(subject, UrlPermission.class, instance.getKeyForPageOrPage("/"));
    }

    /**
     * Devolve str1 se essa for diferente de <code>null</code>, senão devolve str2.
     * 
     * @param str1 string 1
     * @param str2 string 2
     * @return str1 se essa for diferente de <code>null</code>, senão devolve str2.
     */
    protected String getNotNull(String str1, String str2) {
        return (str1 != null) ? str1 : str2;
    }

    /**
     * Devolve o logger.
     * 
     * @return Logger
     */
    protected Logger getLogger() {
        if (SecurityServletFilter.logger == null) {
            SecurityServletFilter.logger = Logger.getLogger(this.getClass());
        }
        return SecurityServletFilter.logger;
    }

    /**
     * @return the loginUrl
     */
    public String getLoginUrl() {
        return this.loginUrl;
    }

    /**
     * @param loginUrl the loginUrl to set
     */
    public void setLoginUrl(String loginUrl) {
        this.loginUrl = loginUrl;
    }

    /**
     * @return the permissionDeniedUrl
     */
    public String getPermissionDeniedUrl() {
        return this.permissionDeniedUrl;
    }

    /**
     * @param permissionDeniedUrl the permissionDeniedUrl to set
     */
    public void setPermissionDeniedUrl(String permissionDeniedUrl) {
        this.permissionDeniedUrl = permissionDeniedUrl;
    }
}
